/* * Copyright (C) 2016 Baidu, Inc. All Rights Reserved. */ package com.dodola.rocoofix; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.AssetManager; import android.os.Build; import android.util.Log; import com.lody.legend.HookManager; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashSet; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.zip.ZipFile; import dalvik.system.DexFile; /** * modify from multidex source code */ public final class RocooFix { static final String TAG = "Rocoo"; private static final String CODE_CACHE_NAME = "code_cache"; private static final String CODE_CACHE_SECONDARY_FOLDER_NAME = "rocoo-dexes"; private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2; private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1; private static final Set<String> installedApk = new HashSet<String>(); private static final String DIR = "rocoo_opt"; private static File mOptDir; private static Map<String, Class<?>> mFixedClass = new ConcurrentHashMap<String, Class<?>>(); private RocooFix() { } public static void init(Context context) { initPathFromAssets(context, "rocoo.dex"); } /** * 另一种方法去掉Preverify标志 beta中 * * @param context */ public static void HackLoader(Context context) { ClassLoaderHack.initHack(context); } /** * 从Assets里取出补丁,一般用于测试 * * @param context * @param assetName */ public static void initPathFromAssets(Context context, String assetName) { File dexDir = new File(context.getFilesDir(), "hotfix"); dexDir.mkdir(); mOptDir = new File(context.getFilesDir(), DIR); if (!mOptDir.exists() && !mOptDir.mkdirs()) {// make directory fail } String dexPath = null; try { dexPath = copyAsset(context, assetName, dexDir); } catch (IOException e) { } finally { if (dexPath != null && new File(dexPath).exists()) { applyPatch(context, dexPath); } } } /** * 从指定目录加载补丁 * * @param context * @param dexPath */ public static void applyPatch(Context context, String dexPath) { // if (IS_VM_CAPABLE) { // //art虚拟机走另外一套fix // return; // } try { ApplicationInfo applicationInfo = getApplicationInfo(context); if (applicationInfo == null) { return; } synchronized (installedApk) { if (installedApk.contains(dexPath)) { return; } installedApk.add(dexPath); /* The patched class loader is expected to be a descendant of * dalvik.system.BaseDexClassLoader. We modify its * dalvik.system.DexPathList pathList field to append additional DEX * file entries. */ ClassLoader loader; try { loader = context.getClassLoader(); } catch (RuntimeException e) { /* Ignore those exceptions so that we don't break tests relying on Context like * a android.test.mock.MockContext or a android.content.ContextWrapper with a * null base Context. */ Log.w(TAG, "Failure while trying to obtain Context class loader. " + "Must be running in test mode. Skip patching.", e); return; } if (loader == null) { // Note, the context class loader is null when running Robolectric tests. Log.e(TAG, "Context class loader is null. Must be running in test mode. " + "Skip patching."); return; } List<File> files = new ArrayList<File>(); files.add(new File(dexPath)); File dexDir = getDexDir(context, applicationInfo); installDexes(loader, dexDir, files); } } catch (Exception e) { e.printStackTrace(); } catch (Throwable e) { e.printStackTrace(); } } private static ApplicationInfo getApplicationInfo(Context context) throws NameNotFoundException { PackageManager pm; String packageName; try { pm = context.getPackageManager(); packageName = context.getPackageName(); } catch (RuntimeException e) { /* Ignore those exceptions so that we don't break tests relying on Context like * a android.test.mock.MockContext or a android.content.ContextWrapper with a null * base Context. */ Log.w(TAG, "Failure while trying to obtain ApplicationInfo from Context. " + "Must be running in test mode. Skip patching.", e); return null; } if (pm == null || packageName == null) { // This is most likely a mock context, so just return without patching. return null; } ApplicationInfo applicationInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA); return applicationInfo; } private static void installDexes(ClassLoader loader, File dexDir, List<File> files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException, InstantiationException, ClassNotFoundException { if (!files.isEmpty()) { if (Build.VERSION.SDK_INT >= 24) { V24.install(loader, files, dexDir); } else if (Build.VERSION.SDK_INT >= 23) { V23.install(loader, files, dexDir); } else if (Build.VERSION.SDK_INT >= 19) { V19.install(loader, files, dexDir); } else if (Build.VERSION.SDK_INT >= 14) { V14.install(loader, files, dexDir); } else { V4.install(loader, files); } } } private static File getDexDir(Context context, ApplicationInfo applicationInfo) throws IOException { File cache = new File(applicationInfo.dataDir, CODE_CACHE_NAME); try { mkdirChecked(cache); } catch (IOException e) { /* If we can't emulate code_cache, then store to filesDir. This means abandoning useless * files on disk if the device ever updates to android 5+. But since this seems to * happen only on some devices running android 2, this should cause no pollution. */ cache = new File(context.getFilesDir(), CODE_CACHE_NAME); mkdirChecked(cache); } File dexDir = new File(cache, CODE_CACHE_SECONDARY_FOLDER_NAME); mkdirChecked(dexDir); return dexDir; } private static void mkdirChecked(File dir) throws IOException { dir.mkdir(); if (!dir.isDirectory()) { File parent = dir.getParentFile(); if (parent == null) { Log.e(TAG, "Failed to create dir " + dir.getPath() + ". Parent file is null."); } else { Log.e(TAG, "Failed to create dir " + dir.getPath() + ". parent file is a dir " + parent.isDirectory() + ", a file " + parent.isFile() + ", exists " + parent.exists() + ", readable " + parent.canRead() + ", writable " + parent.canWrite()); } throw new IOException("Failed to create directory " + dir.getPath()); } } private static final class V23 { private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, InstantiationException { Field pathListField = RocooUtils.findField(loader, "pathList"); Object dexPathList = pathListField.get(loader); Field dexElement = RocooUtils.findField(dexPathList, "dexElements"); Class<?> elementType = dexElement.getType().getComponentType(); Method loadDex = RocooUtils.findMethod(dexPathList, "loadDexFile", File.class, File.class); loadDex.setAccessible(true); Object dex = loadDex.invoke(null, additionalClassPathEntries.get(0), optimizedDirectory); Constructor<?> constructor = elementType.getConstructor(File.class, boolean.class, File.class, DexFile.class); constructor.setAccessible(true); Object element = constructor.newInstance(new File(""), false, additionalClassPathEntries.get(0), dex); Object[] newEles = new Object[1]; newEles[0] = element; RocooUtils.expandFieldArray(dexPathList, "dexElements", newEles); } } private static final class V24 { private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, InstantiationException, ClassNotFoundException { Field pathListField = RocooUtils.findField(loader, "pathList"); Object dexPathList = pathListField.get(loader); Field dexElement = RocooUtils.findField(dexPathList, "dexElements"); Class<?> elementType = dexElement.getType().getComponentType(); Method loadDex = RocooUtils.findMethod(dexPathList, "loadDexFile", File.class, File.class, ClassLoader.class, dexElement.getType()); loadDex.setAccessible(true); Object dex = loadDex.invoke(null, additionalClassPathEntries.get(0), optimizedDirectory, loader, dexElement.get(dexPathList)); Constructor<?> constructor = elementType.getConstructor(File.class, boolean.class, File.class, DexFile.class); constructor.setAccessible(true); Object element = constructor.newInstance(new File(""), false, additionalClassPathEntries.get(0), dex); Object[] newEles = new Object[1]; newEles[0] = element; RocooUtils.expandFieldArray(dexPathList, "dexElements", newEles); } } /** * Installer for platform versions 19. */ private static final class V19 { private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException { /* The patched class loader is expected to be a descendant of * dalvik.system.BaseDexClassLoader. We modify its * dalvik.system.DexPathList pathList field to append additional DEX * file entries. */ Field pathListField = RocooUtils.findField(loader, "pathList"); Object dexPathList = pathListField.get(loader); ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); RocooUtils.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList<File>(additionalClassPathEntries), optimizedDirectory, suppressedExceptions)); if (suppressedExceptions.size() > 0) { for (IOException e : suppressedExceptions) { Log.w(TAG, "Exception in makeDexElement", e); } Field suppressedExceptionsField = RocooUtils.findField(dexPathList, "dexElementsSuppressedExceptions"); IOException[] dexElementsSuppressedExceptions = (IOException[]) suppressedExceptionsField.get(dexPathList); if (dexElementsSuppressedExceptions == null) { dexElementsSuppressedExceptions = suppressedExceptions.toArray( new IOException[suppressedExceptions.size()]); } else { IOException[] combined = new IOException[suppressedExceptions.size() + dexElementsSuppressedExceptions.length]; suppressedExceptions.toArray(combined); System.arraycopy(dexElementsSuppressedExceptions, 0, combined, suppressedExceptions.size(), dexElementsSuppressedExceptions.length); dexElementsSuppressedExceptions = combined; } suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions); } } /** * A wrapper around * {@code private static final dalvik.system.DexPathList#makeDexElements}. */ private static Object[] makeDexElements( Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { Method makeDexElements = RocooUtils.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class); return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions); } } /** * Installer for platform versions 14, 15, 16, 17 and 18. */ private static final class V14 { private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException { /* The patched class loader is expected to be a descendant of * dalvik.system.BaseDexClassLoader. We modify its * dalvik.system.DexPathList pathList field to append additional DEX * file entries. */ Field pathListField = RocooUtils.findField(loader, "pathList"); Object dexPathList = pathListField.get(loader); RocooUtils.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList<File>(additionalClassPathEntries), optimizedDirectory)); } /** * A wrapper around * {@code private static final dalvik.system.DexPathList#makeDexElements}. */ private static Object[] makeDexElements( Object dexPathList, ArrayList<File> files, File optimizedDirectory) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { Method makeDexElements = RocooUtils.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class); return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory); } } /** * Installer for platform versions 4 to 13. */ private static final class V4 { private static void install(ClassLoader loader, List<File> additionalClassPathEntries) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, IOException { /* The patched class loader is expected to be a descendant of * dalvik.system.DexClassLoader. We modify its * fields mPaths, mFiles, mZips and mDexs to append additional DEX * file entries. */ int extraSize = additionalClassPathEntries.size(); Field pathField = RocooUtils.findField(loader, "path"); StringBuilder path = new StringBuilder((String) pathField.get(loader)); String[] extraPaths = new String[extraSize]; File[] extraFiles = new File[extraSize]; ZipFile[] extraZips = new ZipFile[extraSize]; DexFile[] extraDexs = new DexFile[extraSize]; for (ListIterator<File> iterator = additionalClassPathEntries.listIterator(); iterator.hasNext(); ) { File additionalEntry = iterator.next(); String entryPath = additionalEntry.getAbsolutePath(); path.append(':').append(entryPath); int index = iterator.previousIndex(); extraPaths[index] = entryPath; extraFiles[index] = additionalEntry; extraZips[index] = new ZipFile(additionalEntry); extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0); } pathField.set(loader, path.toString()); RocooUtils.expandFieldArray(loader, "mPaths", extraPaths); RocooUtils.expandFieldArray(loader, "mFiles", extraFiles); RocooUtils.expandFieldArray(loader, "mZips", extraZips); RocooUtils.expandFieldArray(loader, "mDexs", extraDexs); } } public static String copyAsset(Context context, String assetName, File dir) throws IOException { File outFile = new File(dir, assetName); if (!outFile.exists()) { AssetManager assetManager = context.getAssets(); InputStream in = assetManager.open(assetName); OutputStream out = new FileOutputStream(outFile); copyFile(in, out); in.close(); out.close(); } return outFile.getAbsolutePath(); } private static void copyFile(InputStream in, OutputStream out) throws IOException { byte[] buffer = new byte[1024]; int read; while ((read = in.read(buffer)) != -1) { out.write(buffer, 0, read); } } /** * 从Asset里加载补丁,一般用于本地测试 * * @param context * @param assetName */ public static void initPathFromAssetsRuntime(Context context, String assetName) { File dexDir = new File(context.getFilesDir(), "hotfix"); dexDir.mkdir(); String dexPath = null; try { dexPath = copyAsset(context, assetName, dexDir); } catch (IOException e) { } finally { if (dexPath != null && new File(dexPath).exists()) { applyPatchRuntime(context, dexPath); } } } /** * 从指定目录加载补丁 * * @param context * @param dexPath */ public static void applyPatchRuntime(Context context, String dexPath) { if (context == null) { return; } else { context = context.getApplicationContext(); } try { File file = new File(dexPath); File optfile = new File(mOptDir, file.getName()); final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(), optfile.getAbsolutePath(), Context.MODE_PRIVATE); ClassLoader classLoader = context.getClassLoader(); ClassLoader patchClassLoader = new ClassLoader(classLoader) { @Override protected Class<?> findClass(String className) throws ClassNotFoundException { Class<?> clazz = dexFile.loadClass(className, this); if (clazz == null && (className.startsWith("com.dodola.rocoofix") || className.startsWith("com.lody.legend") || className.startsWith("com.alipay.euler.andfix") )) { return Class.forName(className); } if (clazz == null) { throw new ClassNotFoundException(className); } return clazz; } }; Enumeration<String> entrys = dexFile.entries(); Class<?> clazz = null; while (entrys.hasMoreElements()) { String entry = entrys.nextElement(); clazz = dexFile.loadClass(entry, patchClassLoader); if (clazz != null) { fixClass(clazz, classLoader); } } } catch (IOException e) { } } private static void fixClass(Class<?> clazz, ClassLoader classLoader) { if (clazz == null) { return; } Method[] methods = clazz.getDeclaredMethods(); try { Class<?> aClass = classLoader.loadClass(clazz.getName()); String key = aClass.getName() + "@" + classLoader.toString(); Class<?> clazzFixed = mFixedClass.get(key); if (clazzFixed == null) { Class<?> clzz = classLoader.loadClass(clazz.getName()); // 他喵的我忘了初始化这个类了 clazzFixed = initTargetClass(clzz); } if (clazzFixed != null) { mFixedClass.put(key, clazzFixed); for (Method fixMethod : methods) { replaceMethod(aClass, fixMethod, classLoader); } } } catch (ClassNotFoundException e) { } catch (NoSuchMethodException e) { e.printStackTrace(); } } public static Class<?> initTargetClass(Class<?> clazz) { try { Class<?> targetClazz = Class.forName(clazz.getName(), true, clazz.getClassLoader()); // initFields(targetClazz); return targetClazz; } catch (Exception e) { } return null; } // private static void initFields(Class<?> clazz) { // Field[] srcFields = clazz.getDeclaredFields(); // for (Field srcField : srcFields) { // setFieldFlag(srcField); // } // } private static void replaceMethod(Class<?> aClass, Method fixMethod, ClassLoader classLoader) throws NoSuchMethodException { try { Method originMethod = aClass.getDeclaredMethod(fixMethod.getName(), fixMethod.getParameterTypes()); HookManager.getDefault().hookMethod(originMethod, fixMethod); } catch (Exception e) { Log.e(TAG, "replaceMethod", e); } } }